/*
 * Copyright 2011 Niklas Kyster Rasmussen
 *
 * This file is part of NiKR Text Library.
 *
 * NiKR Text Library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * NiKR Text Library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with NiKR Text Library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 */
package net.nikr.text;

import java.io.IOException;
import java.io.Writer;
import java.text.Format;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * 
 * @author Niklas
 */
public final class TextTable {

	private List<List<Cell>> rows = new ArrayList<List<Cell>>();
	private List<List<Cell>> columns = new ArrayList<List<Cell>>();
	private List<Cell> currentRow = null;
	private Map<Class, Format> formats = new HashMap<Class, Format>();
	private int spacing = 2;
	/**
	 * Cell Alignment
	 */
	public enum Alignment {
		/**
		 * Left alignment 
		 */
		LEFT,
		/**
		 * Center alignment 
		 */
		CENTER,
		/**
		 * Right alignment
		 */
		RIGHT
	}
	
	
	/**
	 * Create new instance of TextTable (Empty)
	 */
	public TextTable() {
		setData(null, new Alignment[0]);
	}
	
	/**
	 * Create new instance of TextTable
	 * @param data	cells to populate the table
	 */
	public TextTable(Object[][] data) {
		setData(data, new Alignment[0]);
	}

	/**
	 * Create new instance of TextTable
	 * @param data	cells to populate the table
	 * @param alignment	Columns alignment
	 */
	public TextTable(Object[][] data, Alignment[] alignment) {
		setData(data, alignment);
	}
	/**
	 * Create new instance of TextTable
	 * @param data	cells to populate the table
	 * @param alignment	Columns alignment
	 */
	public TextTable(Object[][] data, Alignment[][] alignment) {
		setData(data, alignment);
	}
	
	/**
	 * Add an Object to current row (see: nextRow())
	 * Use toString() to get string
	 * @param obj		Object to add
	 * @return			this Table
	 */
	public TextTable add(Object obj) {
		add(obj, Alignment.LEFT);
		return this;
	}

	/**
	 * Add Object to current row (see: nextRow())
	 * Use toString() to get string
	 * @param obj		Object to add
	 * @param align		Cell alignment
	 * @return			this Table
	 */
	public TextTable add(Object obj, Alignment align) {
		if (currentRow == null) nextRow();
		Cell cell = new Cell(obj, align, formats);
		currentRow.add(cell);
		List<Cell> column;
		if (currentRow.size() > columns.size()) {
			column = new ArrayList<Cell>();
			columns.add(column);
		} else {
			column = columns.get(currentRow.size() - 1);
		}
		column.add(cell);
		return this;
	}
	
	/**
	 * Add all to table
	 * @param data		Object(s) to add [rows][cells]
	 */
	public void addAll(Object[][] data){
		add(data, null);
	}
	
	/**
	 * Add all to table
	 * @param data		Object(s) to add [rows][cells]
	 * @param alignment	Cells alignment
	 */
	public void addAll(Object[][] data, Alignment[][] alignment){
		if (data != null) {
			for (int i = 0; i < data.length; i++) {
				addAll(data[i], alignment[i]);
			}
		}
	}
	
	/**
	 * Add all to table
	 * @param data		Object(s) to add [rows][cells]
	 * @param alignment	Columns alignment
	 */
	public void addAll(Object[][] data, Alignment[] alignment){
		if (data != null) {
			for (Object[] row : data) {
				addAll(row, alignment);
			}
		}
	}
	
	/**
	 * Add all to table
	 * @param data		Object(s) to add [cells]
	 */
	public void addAll(Object[] data){
		addAll(data, null);
	}
	
	/**
	 * Add all to table
	 * @param data		Object(s) to add [cells]
	 * @param alignment	Columns alignment
	 */
	public void addAll(Object[] data, Alignment[] alignment){
		nextRow();
		for (Object value : data) {
			if (alignment != null && currentRow.size() < alignment.length){
				add(value, alignment[currentRow.size()]);
			} else {
				add(value);
			}
		}
	}
	
	/**
	 * Clear all data from the table
	 */
	public void clear(){
		rows = new ArrayList<List<Cell>>();
		columns = new ArrayList<List<Cell>>();
		currentRow = null;
	}
	
	/**
	 * Clear all set formats
	 */
	public void clearFormats(){
		formats = new HashMap<Class, Format>();
		updateFormats();
	}
	
	/**
	 * Get number of columns
	 * @return			columns count
	 */
	public int columnCount() {
		return columns.size();
	}
	
	/**
	 * Get default Format for a Class
	 * @param clazz		Class
	 * @return			Format or null (if none is set)
	 */
	public Format getDefaultFormat(Class clazz){
		return formats.get(clazz);
	}
	
	/**
	 * Is table empty
	 * @return			true if table is empty, otherwise false
	 */
	public boolean isEmpty(){
		return (rows.isEmpty() && columns.isEmpty());
	}
	
	/**
	 * Start a new row (used with add(Object obj))
	 */
	public void nextRow() {
		currentRow = new ArrayList<Cell>();
		rows.add(currentRow);
	}
	
	/**
	 * Get number of rows
	 * @return			row count
	 */
	public int rowCount() {
		return rows.size();
	}
	
	/**
	 * Overwrite table data
	 * @param data		Object(s) to add [rows][cells]
	 */
	public void setData(Object[][] data){
		setData(data, new Alignment[0]);
	}
	
	
	/**
	 * Overwrite table data
	 * @param data		Object(s) to add [rows][cells]
	 * @param alignment	Columns alignment
	 */
	public void setData(Object[][] data, Alignment[] alignment){
		clear();
		addAll(data, alignment);
	}
	
	
	/**
	 * Overwrite table data
	 * @param data		Object(s) to add [rows][cells]
	 * @param alignment	Cell alignment
	 */
	public void setData(Object[][] data, Alignment[][] alignment){
		clear();
		addAll(data, alignment);
	}
	
	/**
	 * Set default Format for a Class
	 * @param clazz		Class to be formated
	 * @param format	Format to format
	 */
	public void setDefaultFormat(Class clazz, Format format){
		formats.put(clazz, format);
		updateFormats();
	}

	/**
	 * Get spacing between columns
	 * @return spacing
	 */
	public int getSpacing() {
		return spacing;
	}

	/**
	 * Set spacing between columns
	 * @param spacing
	 */
	public void setSpacing(int spacing) {
		this.spacing = spacing;
	}

	/**
	 * Write table data to System.out
	 */
	public void write() {
		try {
			write(null);
		} catch (IOException ex) {
			
		}
	}
	
	/**
	 * Write table data to Writer
	 * @param writer	Writer to write to
	 * @throws IOException
	 */
	public void write(Writer writer) throws IOException {
		for (List<Cell> row : rows) {
			for (int i = 0; i < row.size(); i++) {
				row.get(i).write(writer, getMaxLength(columns.get(i)), spacing, i == 0);
			}
			if (writer != null){
				writer.write(System.getProperty("line.separator"));
			} else {
				System.out.print(System.getProperty("line.separator"));
			}
		}
	}
	
	private void updateFormats(){
		for (List<Cell> row : rows) {
			for (Cell cell : row){
				cell.updateFormat(formats);
			}
		}
	}
	
	private int getMaxLength(List<Cell> cells){
		int lenght = 0;
		for (Cell cell : cells){
			if (cell.getLength() > lenght){
				lenght = cell.getLength();
			}
		}
		return lenght;
	}
	
	private static class Cell {
		private Object obj;
		private String s;
		private Alignment align;

		public Cell(Object obj, Alignment align, Map<Class, Format> formats) {
			this.obj = obj;
			this.align = align;
			updateFormat(formats);
		}

		public void write(Writer writer, int maxLength, int spacing, boolean first) throws IOException{
			int length = maxLength - s.length();
			switch (align){
				case LEFT:
					write(writer, (first ? "" : space(spacing))+s+space(length));
					break;
				case CENTER:
					write(writer, (first ? "" : space(spacing))+space(length/2)+s+space(length/2)+((length % 2 != 0) ? " " : ""));
					break;
				case RIGHT:
					write(writer, (first ? "" : space(spacing))+space(length)+s);
			}
		}

		private void write(Writer writer, String s) throws IOException{
			if (writer != null){
				writer.write(s);
			} else {
				System.out.print(s);
			}
		}

		private String space(int length){
			String sTemp = "";
			for (int i = 0; i < length; i++){
				sTemp = sTemp + " ";
			}
			return sTemp;
		}

		final void updateFormat(Map<Class, Format> formats){
			Format format = formats.get(obj.getClass());
			if (format != null){
				s = format.format(obj);
			} else {
				s = obj.toString();
			}
		}

		int getLength(){
			return s.length();
		}
	}
}
